主题
Go 接口
一个函数只是把结果写出去,为什么参数一定要是文件?如果它只需要“能写入一段字节”,就可以把参数声明成 io.Writer,文件、内存缓冲区和测试替身都能交给它。接口的价值在这里:让调用方只依赖要用的能力。
接口也带来几种需要单独理解的行为:any 能装下整数,不代表能直接做整数运算;一个指针是 nil,放进接口后接口却可能不是 nil;两个接口都装着 slice,比较时可能 panic。这篇从一次写入操作讲起,再把动态类型、断言和 nil 串起来。
| 遇到的需求 | 对应机制 | 最容易误判的地方 |
|---|---|---|
| 接受不同实现的同一种能力 | 方法接口、隐式实现 | 方法名相同还不够,签名也必须匹配 |
| 暂时容纳类型未知的值 | any | 它保留实际类型,不会把值变成“通用类型” |
| 想知道里面是什么 | 类型断言、type switch | 断言是检查现有类型,不是做数值转换 |
| 判断是否真的没有值 | nil 接口 | 有类型的 nil 指针仍会形成非 nil 接口 |
| 比较两个接口 | 动态类型与动态值比较 | 动态值不可比较时,比较可能 panic |
以下例子实际运行于 Go 1.27.2、Linux/amd64。本文主要讨论可作为普通变量和参数的接口;泛型约束与运行时接口的边界也会交代。文末保留完整程序、测试和故意编译失败的代码。
从 io.Writer 看隐式实现
io.Writer 只有一个方法:[1]
go
type Writer interface {
Write(p []byte) (n int, err error)
}一个类型只要方法集满足这个签名,就实现了 io.Writer,不需要写 implements,也不需要修改那个接口所在的包。于是可以写一个只负责生成文本的函数:[2]
go
func WriteCount(w io.Writer, n int) error {
_, err := fmt.Fprintf(w, "count=%d\n", n)
return err
}
var buf bytes.Buffer
var text strings.Builder
err1 := WriteCount(&buf, 3)
err2 := WriteCount(&text, 3)
fmt.Printf("%q %q %v %v\n", buf.String(), text.String(), err1, err2)text
"count=3\n" "count=3\n" <nil> <nil>*bytes.Buffer 和 *strings.Builder 都有符合要求的 Write 方法。WriteCount 不用知道具体类型,只调用接口提供的写入能力;实现决定把字节存到哪里。写文件时换成满足同一接口的对象即可,不必重写生成文本的逻辑。[3][4]
正在绘制图示…
查看图源代码
flowchart LR
C["调用方选择对象"] --> W["WriteCount<br/>参数 io.Writer"]
W --> F["fmt.Fprintf<br/>生成 count=3 的文本"]
F --> M["调用具体对象的 Write"]
M --> B["*bytes.Buffer<br/>写入内存缓冲"]
M --> S["*strings.Builder<br/>构建字符串"]
图画的是语义上的调用与数据去向,不表示每次调用都会同时写到两个对象。具体实现只有调用方传进来的那一个;编译器如何优化调用,不改变这个约定。
签名满足了,行为约定还要实现者自己守住
下面只有方法名相同,却没有实现接口:
go
type BadWriter struct{}
func (BadWriter) Write([]byte) error { return nil }
var _ io.Writer = BadWriter{}实际编译诊断中的关键部分是:
text
BadWriter does not implement io.Writer (wrong type for method Write)
have Write([]byte) error
want Write([]byte) (int, error)参数和返回值类型都是接口契约的一部分。参数名可以不同,但不能漏掉返回的字节数,也不能擅自换成另一个返回类型。[2:1]
编译器只能检查类型层面的匹配。io.Writer 文档还规定:返回的 n 小于输入长度时必须有非 nil 错误;不能修改输入字节,也不能保留那份输入 slice。这些行为并不自动由“实现了接口”保证,需要实现者遵守,并由测试检查。[1:1]
实现接口的是 T,还是 *T
判断隐式实现时,具体类型也要看完整。值类型 T 的方法集包含值接收者方法,*T 的方法集还包含指针接收者方法。因此下面几种情况不同:[5]
| 具体类型 | 是否实现 io.Writer | 原因 |
|---|---|---|
bytes.Buffer | 否 | Write 是指针接收者方法 |
*bytes.Buffer | 是 | 方法集中有 Write([]byte) (int, error) |
本文的 WriterFunc | 是 | 适配器将 Write 定义在值接收者上 |
即使局部变量 buf 能直接写 buf.Write(p),也不能据此把 buf 值传给 WriteCount。前一种调用利用了可寻址变量自动取址的便利,后一种向接口赋值仍检查值类型的方法集,所以这里传的是 &buf。文末反例明确检查 var _ io.Writer = bytes.Buffer{} 会被拒绝。完整的方法集与调用语法区别见 Go 结构体与方法。[5:1][3:1]
小接口:按调用方的需要收窄能力
如果 WriteCount 的参数写成 *os.File,调用方就得准备文件,测试时也会平白多出文件创建和清理。写成拥有读、写、关闭、定位等一整套方法的大接口,又会要求测试对象实现它根本用不到的方法。io.Writer 只有一个方法,恰好足够完成当前工作。[6]
| 参数设计 | 调用方要提供什么 | 合适的场景 |
|---|---|---|
具体类型 *T | 这个确定的对象 | 确实依赖该对象特有能力,抽象暂时没有收益 |
小接口,如 io.Writer | 当前操作实际需要的方法 | 需要替换实现、隔离外部依赖或编写测试 |
| 多方法接口 | 同时具备整组能力的对象 | 调用方确实把这组能力作为一个整体使用 |
any | 几乎任意值 | 类型确实未知,后续有明确的识别和处理规则 |
这里“小”是职责窄,不是机械要求所有接口只能有一个方法。io.ReadWriter 同时需要读和写就很合理。用户代码里的接口通常更适合由使用它的一方定义,方法由实际调用需求决定;不要在没有使用场景时为每个结构体先配一个接口。Go 官方代码审查建议也采用这个方向。[6:1][7]
测试失败路径,不必搭一个真的故障服务
完整代码里定义了一个把函数适配为 Writer 的类型:
go
type WriterFunc func([]byte) (int, error)
func (f WriterFunc) Write(p []byte) (int, error) { return f(p) }它不是结构体,照样可以有方法并满足接口。让它在每次写入时返回指定错误,就能直接检查失败是否传回调用方:
go
sentinel := errors.New("blocked")
fail := WriterFunc(func([]byte) (int, error) { return 0, sentinel })
fmt.Println("write failure:", errors.Is(WriteCount(fail, 3), sentinel))text
write failure: true这里没有创建文件,也没有真的让磁盘发生故障。WriteCount 只需要一个 Writer,测试就能把“写入失败”作为确定输入传给它。文末 TestWriteFailure 实际通过,验证了这条失败路径。类型关系由编译器检查,业务行为由测试检查,两者承担不同工作。[8]
如果生产函数收到接口后立刻断言回某个具体类型,而且没有别的实现真的可用,接口可能没有带来多少好处。此时直接接收具体类型,或者把真正需要的方法补进接口,往往比到处写断言更清楚。这是设计建议,不是语言禁令。
接口值:静态类型规定能做什么,动态类型记录装的是什么
声明 var w io.Writer 时,变量的静态类型就是 io.Writer,编译器据此允许调用 Write。把 &buf 赋给它之后,接口值又有一个动态类型 *bytes.Buffer,以及对应的动态值,也就是那个指针值。[9]
可以用 (动态类型, 动态值) 理解接口的语义:
正在绘制图示…
查看图源代码
flowchart LR
I["变量 w<br/>静态类型 io.Writer"] --> T["动态类型<br/>*bytes.Buffer"]
I --> V["动态值<br/>指向 buf 的指针"]
V --> B["具体的 buf 对象"]
这解释了一个重要限制:即使里面装着 *bytes.Buffer,通过静态类型 io.Writer 也不能直接调用 String(),因为接口没有声明这个方法。若调用方确实需要 String(),应该调整参数契约,或者在确有可选能力需求时使用类型断言。[9:1][10]
接口再赋给接口,不会套一层“接口盒子”
go
var buf bytes.Buffer
var w io.Writer = &buf
var x any = w
fmt.Printf("%T %T\n", w, x)text
*bytes.Buffer *bytes.Bufferx 的动态类型仍是 *bytes.Buffer,不是 io.Writer。接口之间赋值保留实际的动态类型和值;接口动态类型是具体的非接口类型,不会形成“接口里面套着接口”的层层结构。fmt 的 %T 显示的是这个实际类型。[9:2][4:1]
给接口赋结构体值时,复制的是值;赋指针时,复制的是指针值。结构体里的 slice、map 等引用仍可能共享,所以“放进接口”不是深复制操作。更完整的复制示例见 Go 类型系统 和 Go 结构体与方法。[11]
语义模型和运行时布局要分开看
(type, value) 是理解行为的模型,不是在要求所有编译器永远按同一块内存布局实现。在本次官方 Go 1.27.2 源码中,运行时使用两种结构:[12]
| 当前运行时结构 | 对应情形 | 包含的信息 |
|---|---|---|
eface | 空接口,如 any | 类型信息指针 _type,以及 data |
iface | 非空方法接口 | 方法表相关指针 tab,以及 data |
itab / abi.ITab | 非空接口使用的表 | 接口类型、具体类型、方法入口等 |
data 用于承载动态值的机器表示,不能一律理解成“用户值的所有位直接放在这一格”。装箱、指针和编译优化会影响具体实现。业务代码不应依赖这些私有字段,也不要用 unsafe 手工拆接口来代替正常断言。本文没有测量接口调用成本或堆分配次数,不会从这张表推导“接口必然分配”或“接口必然很慢”。[12:1]
any:不要求方法,但实际类型还在
any 是 interface{} 的别名,两种写法完全等价。空接口不要求方法,所以可以装整数、字符串、结构体、slice 等值;变量的静态类型仍然是接口,不会跟着每次赋值改成动态类型。[13]
go
var x any = 42
n, ok := x.(int)
fmt.Println(n, ok)
s, ok := x.(string)
fmt.Printf("%q %t\n", s, ok)text
42 true
"" false要按整数使用它,先检查并取出整数。直接写 x + 1 会编译失败,原因是编译器看到的是 any,无法保证里面是能相加的值。any 把部分检查推迟到了运行时,没有让静态类型规则消失。[13:1][14]
一个 []int 可以放进 any,不等于它就是 []any
go
ints := []int{1, 2}
var whole any = ints
values := make([]any, len(ints))
for i, n := range ints { values[i] = n }
ints[0] = 9
fmt.Printf("%T %v | %T %v\n", whole, whole, values, values)text
[]int [9 2] | []interface {} [1 2]whole 装的是整份 []int 的 slice 值,底层数组仍共享,所以能看到后来写入的 9。values 则是另一个 slice,其中每个元素都是接口;循环逐项把整数复制了进去,所以还保留 1、2。两种 slice 的元素类型不同,不能直接写 var values []any = ints。文末编译器检查了这个错误。[11:1][15]
any 适合值的种类确实未知的边界,例如通用格式化入口。业务模型已经确定时,用明确字段和明确参数通常更容易检查错误。如果一个算法只需要保留“所有元素是同一种 T”而不是运行时混装各种值,可以进一步考虑泛型,见 Go 泛型。这属于 API 选择,不是说接口和泛型只能二选一。
类型断言与 type switch:检查类型,不是转换类型
先分清两个断言目标
| 写法 | 成功的条件 | 失败时怎样处理 |
|---|---|---|
v := x.(T),T 是具体类型 | 动态类型就是 T | 运行时 panic |
v, ok := x.(T),T 是具体类型 | 动态类型就是 T | ok=false,v 为 T 的零值 |
v, ok := x.(I),I 是接口 | 动态类型实现 I | 不实现时返回 false,v 为 nil 接口 |
断言要求 x 是接口表达式。两返回值形式不会因为断言失败而 panic,适合可预期的类型分支;单返回值形式意味着你已经建立了足够强的前提,前提被破坏就让程序失败。[14:1]
go
type UserID int64
var id any = UserID(7)
_, plain := id.(int64)
typed, correct := id.(UserID)
fmt.Println(plain, typed, correct)
var buf bytes.Buffer
var a any = &buf
_, writable := a.(io.Writer)
fmt.Println(writable)text
false 7 true
trueUserID 与 int64 是不同的定义类型,底层类型一样也不能让第一条断言成功。要转换,应先取出 UserID,再显式转成 int64。后面的断言目标是接口,不要求动态类型名字叫 io.Writer;只要 *bytes.Buffer 实现了它就可以。[14:2][16]
nil 接口没有动态类型,所以对它做具体类型断言同样失败;有类型的 nil 指针却可能成功断言为对应指针类型。这一点在下一节用错误接口验证。
多个类型分支用 type switch
go
func describe(x any) string {
switch v := x.(type) {
case nil:
return "nil interface"
case int:
return fmt.Sprintf("int %d", v)
case string:
return fmt.Sprintf("string %q", v)
case *Problem:
if v == nil { return "nil *Problem" }
return "problem " + v.Message
default:
return fmt.Sprintf("other %T", v)
}
}依次传入 nil、42、"go"、(*Problem)(nil)、UserID(7),实际输出:
text
nil interface
int 42
string "go"
nil *Problem
other main.UserID单一类型分支里的 v 具有该分支的类型,因此 case string 可以直接按字符串处理,case *Problem 可以访问字段。case nil 只匹配 nil 接口;带着类型的 nil 指针会进入指针分支。UserID 也不会因为底层是整数就进入 case int。[17]
接口类型也可以出现在 case 中,若多个 case 都可能匹配,按源码顺序选择第一个匹配分支。需要区分具体类型时,把宽泛接口分支放在哪里必须有意识地决定;case any 放在前面会截住后面的所有非 nil 具体值。[17:1]
nil 接口与有类型的 nil:错误处理最常见的陷阱
先定义一个自定义错误。这里特意让方法可以处理 nil 接收者,便于观察而不让程序中途结束:
go
type Problem struct { Message string }
func (p *Problem) Error() string {
if p == nil { return "<nil *Problem>" }
return p.Message
}
var p *Problem
var err error = p
var x any = err
fmt.Println(p == nil, err == nil, x == nil)
fmt.Printf("%T %T\n", err, x)
fmt.Println(err.Error())text
true false false
*main.Problem *main.Problem
<nil *Problem>p 确实是 nil。把它放进 error 后,接口记录的是 (*Problem, nil),动态类型已经存在,所以接口不等于 nil。再赋给 any 也保留这个动态类型和值,不会自动“清空”。Error 能正常返回,是因为方法显式检查了 nil,不是接口替它做了保护。[18][19]
正在绘制图示…
查看图源代码
flowchart LR
E["var empty error"] --> ET["动态类型:无"]
E --> EV["动态值:无<br/>empty == nil 为 true"]
N["var p *Problem = nil<br/>var err error = p"] --> NT["动态类型:*Problem"]
N --> NV["动态值:nil 指针<br/>err == nil 为 false"]
| 状态 | 动态类型 | 动态值 | 与 nil 比较 |
|---|---|---|---|
var err error | 无 | 无 | true |
err = (*Problem)(nil) | *Problem | nil 指针 | false |
err = &Problem{"failed"} | *Problem | 指向错误对象的指针 | false |
err = nil | 无 | 无 | true |
err != nil 只说明接口本身不为 nil,不能保证里面的指针非 nil。反过来,调用真正 nil 接口上的方法会 panic,因为它根本没有可调用的动态类型。文末测试分别检查了 nil 接口调用和 typed nil 断言,不把它们混成一种错误。[18:1][10:1]
返回 error 时,成功分支要明确 return nil
go
func badResult(ok bool) error {
var p *Problem
if !ok { p = &Problem{Message: "failed"} }
return p
}
func goodResult(ok bool) error {
if !ok { return &Problem{Message: "failed"} }
return nil
}
fmt.Println(badResult(true) == nil, goodResult(true) == nil)text
false true第一种写法在成功时返回了一个 nil *Problem,函数返回类型却是 error,返回过程中把它装成带类型的接口。调用方按正常的 if err != nil 检查,就误以为失败了。第二种直接返回 nil 接口,才符合 error 约定中的“没有错误”。Go FAQ 用同样的机制解释这一问题。[18:2][20]
最简单的修法通常在生产接口值的地方:成功明确返回 nil,构造函数写清是否可能返回 nil,别把“可选对象”藏进模糊的接口里。不要为了兜底就对每个参数调用 reflect.ValueOf(x).IsNil(),它只支持若干可为 nil 的种类,对整数等值或无效的反射值会 panic。反射不是不看类型就能通用判空的捷径。[21]
接口可以比较,不代表里面的值总能比较
go
var a any = int(1)
var b any = int64(1)
var c any = int(1)
fmt.Println(a == b, a == c)
var s any = []int{1}
fmt.Println(s == nil)text
false true
false两个非 nil 接口要相等,动态类型必须相同,动态值也必须相等。int(1) 和 int64(1) 类型不同,所以结果是 false。slice 放进接口后,与 nil 比较是安全的,结果是 false,因为接口有动态类型。[22]
但若两边的动态类型都是 []int,继续比较动态值时会遇到不可比较类型,于是 panic。文末 slice equality 子测试验证了这一点。包含接口字段的结构体、以接口作键的 map,也可能把这类检查推迟到运行时,不能因为外层类型通过编译,就假定所有输入都安全。[22:1]
如果要比较两个序列的元素,应该取出具体序列并使用适合元素类型的比较方法;如果要作为键,应在边界规定允许的键类型。any 提供的是接纳能力,不是相等、排序或计算规则。
泛型约束也写 interface,但不都能创建运行时接口值
下面两种声明都用了 interface,用途不同:[23]
| 声明 | 能否作为普通参数、变量类型 | 主要用途 |
|---|---|---|
interface { Write([]byte) (int, error) } | 可以 | 用方法表达运行时可替换的能力 |
interface { ~int | ~int64 } | 不可以 | 作为类型参数约束,限制允许的类型 |
带类型项或相应限制的非基本接口只能作为约束使用,不能写 var n Number 然后往里面放整数。本篇反例会得到 cannot use type Number outside a type constraint。泛型怎样用约束统一处理一组类型,放到 Go 泛型 展开。
接口的设计可以从调用点检查:这个函数到底要调用哪些方法?这些方法的行为由谁保证?返回的 nil 是不是接口本身的 nil?如果需要恢复具体类型,是否真的在处理一个可选能力?把这些问题答清楚,接口就能减少依赖,而不是把原本可在编译期发现的错误推到运行时。
复现与验证结果
把下文四份文件按路径保存后运行:
bash
go version
go run .
go test -count=1 -v .
go test -count=1 -race .
go vet .
go build -o invalid.a testdata/compile_fail.go本次环境为 Go 1.27.2、Linux/amd64、CGO_ENABLED=1。正常程序、6 个 Example、3 个 Test、race、vet 均通过。Example 会逐行核对正文输出;其他测试覆盖 Writer 失败路径、断言 panic、nil 接口调用、slice 接口比较和 typed nil 断言。race 仅覆盖执行到的代码,本篇没有测量性能或证明任意接口实现都没有竞争。[24]
最后一条故意编译失败,退出码为 1,验证五类错误:Writer 签名不符、值类型缺少指针接收者方法、直接给 any 加 1、[]int 直接赋给 []any、把非基本接口当普通变量类型。除前文展示的 Writer 诊断外,其他核心信息为:
text
invalid operation: x + 1 (mismatched types any and untyped int)
cannot use ints (variable of type []int) as []any value in variable declaration
cannot use type Number outside a type constraint: interface contains type constraints完整可复现代码
go.mod
go
module example.com/go-interfaces
go 1.27.0main.go
代码块 · 141 行 · 点击收起 / 展开
go
package main
import (
"bytes"
"errors"
"fmt"
"io"
"strings"
)
func WriteCount(w io.Writer, n int) error {
_, err := fmt.Fprintf(w, "count=%d\n", n)
return err
}
type WriterFunc func([]byte) (int, error)
func (f WriterFunc) Write(p []byte) (int, error) { return f(p) }
var _ io.Writer = WriterFunc(nil)
func writers() {
var buf bytes.Buffer
var text strings.Builder
err1 := WriteCount(&buf, 3)
err2 := WriteCount(&text, 3)
fmt.Printf("%q %q %v %v\n", buf.String(), text.String(), err1, err2)
var w io.Writer = &buf
var x any = w
fmt.Printf("%T %T\n", w, x)
sentinel := errors.New("blocked")
fail := WriterFunc(func([]byte) (int, error) { return 0, sentinel })
fmt.Println("write failure:", errors.Is(WriteCount(fail, 3), sentinel))
}
type UserID int64
func assertions() {
var x any = 42
n, ok := x.(int)
fmt.Println(n, ok)
s, ok := x.(string)
fmt.Printf("%q %t\n", s, ok)
var id any = UserID(7)
_, plain := id.(int64)
typed, correct := id.(UserID)
fmt.Println(plain, typed, correct)
var buf bytes.Buffer
var a any = &buf
_, writable := a.(io.Writer)
fmt.Println(writable)
}
func anySlices() {
ints := []int{1, 2}
var whole any = ints
values := make([]any, len(ints))
for i, n := range ints {
values[i] = n
}
ints[0] = 9
fmt.Printf("%T %v | %T %v\n", whole, whole, values, values)
}
type Problem struct{ Message string }
func (p *Problem) Error() string {
if p == nil {
return "<nil *Problem>"
}
return p.Message
}
func badResult(ok bool) error {
var p *Problem
if !ok {
p = &Problem{Message: "failed"}
}
return p
}
func goodResult(ok bool) error {
if !ok {
return &Problem{Message: "failed"}
}
return nil
}
func nilInterfaces() {
var p *Problem
var err error = p
var x any = err
fmt.Println(p == nil, err == nil, x == nil)
fmt.Printf("%T %T\n", err, x)
fmt.Println(err.Error())
fmt.Println(badResult(true) == nil, goodResult(true) == nil)
var empty error
fmt.Println(empty == nil)
}
func describe(x any) string {
switch v := x.(type) {
case nil:
return "nil interface"
case int:
return fmt.Sprintf("int %d", v)
case string:
return fmt.Sprintf("string %q", v)
case *Problem:
if v == nil {
return "nil *Problem"
}
return "problem " + v.Message
default:
return fmt.Sprintf("other %T", v)
}
}
func switches() {
for _, v := range []any{nil, 42, "go", (*Problem)(nil), UserID(7)} {
fmt.Println(describe(v))
}
}
func comparisons() {
var a any = int(1)
var b any = int64(1)
var c any = int(1)
fmt.Println(a == b, a == c)
var s any = []int{1}
fmt.Println(s == nil)
}
func main() {
for _, demo := range []struct {
name string
run func()
}{
{"writers", writers}, {"assertions", assertions}, {"any slices", anySlices},
{"nil interfaces", nilInterfaces}, {"switches", switches},
{"comparisons", comparisons},
} {
fmt.Println("==", demo.name, "==")
demo.run()
}
}main_test.go
代码块 · 86 行 · 点击收起 / 展开
go
package main
import (
"errors"
"testing"
)
func Example_writers() {
writers()
// Output:
// "count=3\n" "count=3\n" <nil> <nil>
// *bytes.Buffer *bytes.Buffer
// write failure: true
}
func Example_assertions() {
assertions()
// Output:
// 42 true
// "" false
// false 7 true
// true
}
func Example_anySlices() {
anySlices()
// Output:
// []int [9 2] | []interface {} [1 2]
}
func Example_nilInterfaces() {
nilInterfaces()
// Output:
// true false false
// *main.Problem *main.Problem
// <nil *Problem>
// false true
// true
}
func Example_switches() {
switches()
// Output:
// nil interface
// int 42
// string "go"
// nil *Problem
// other main.UserID
}
func Example_comparisons() {
comparisons()
// Output:
// false true
// false
}
func TestWriteFailure(t *testing.T) {
sentinel := errors.New("blocked")
fail := WriterFunc(func([]byte) (int, error) { return 0, sentinel })
if err := WriteCount(fail, 3); !errors.Is(err, sentinel) {
t.Fatalf("got %v", err)
}
}
func TestPanics(t *testing.T) {
for _, tc := range []struct {
name string
run func()
}{
{"failed assertion", func() { var x any = 42; _ = x.(string) }},
{"nil assertion", func() { var x any; _ = x.(int) }},
{"nil interface call", func() { var err error; _ = err.Error() }},
{"slice equality", func() { var a any = []int{1}; var b any = []int{1}; _ = a == b }},
} {
t.Run(tc.name, func(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("expected panic")
}
}()
tc.run()
})
}
}
func TestTypedNilAssertion(t *testing.T) {
var err error = (*Problem)(nil)
p, ok := err.(*Problem)
if !ok || p != nil || err == nil {
t.Fatal("typed nil semantics changed")
}
}testdata/compile_fail.go
go
package fail
import (
"bytes"
"io"
)
type BadWriter struct{}
func (BadWriter) Write([]byte) error { return nil }
var _ io.Writer = BadWriter{}
var _ io.Writer = bytes.Buffer{}
func invalid() {
var x any = 1
_ = x + 1
ints := []int{1, 2}
var values []any = ints
_ = values
}
type Number interface{ ~int | ~int64 }
var n Number依据
语言规则与本次官方 Go 1.27.2 发行包中的 doc/go_spec.html 核对,接口表示另检查固定版本运行时源码。文中没有把私有布局当成语言承诺。
bytes.Buffer.Write、strings.Builder.Write;二者接收者均为指针。 ↩︎ ↩︎fmt.Fprintf与fmt格式动词:%T打印 Go 语法形式的类型,%q显示带转义的字符串。 ↩︎ ↩︎Go 官方 Wiki:Interfaces,建议接口通常放在消费方,不要在缺少实际使用场景时预先定义;Effective Go 的接口说明。本篇表中的取舍是依据这些原则给出的设计建议。 ↩︎ ↩︎
io.ReadWriter:组合 Reader 和 Writer 两组方法。 ↩︎Go 规范:变量的静态类型与动态类型、接口类型;官方文章 The Laws of Reflection 解释接口携带具体类型和值,以及接口不持有接口类型动态值。 ↩︎ ↩︎ ↩︎
固定证据为 Go 1.27.2 官方发行包 内
src/runtime/runtime2.go第 184 行的iface、第 189 行的eface,以及src/internal/abi/iface.go第 17 行的ITab。本次已读取这些定义;发行包核对的 SHA-256 为ecbadb99091a3f46e31f5f934b068b1864eafa7995211b39eaddf76996045fe5。 ↩︎ ↩︎builtin.any;Go 规范:基本接口,any是interface{}的别名。 ↩︎ ↩︎Go 规范:type switch,分支匹配顺序、nil 分支和分支变量的类型规则。 ↩︎ ↩︎
Go FAQ:为什么 error 是 nil 却不等于 nil;Go 规范:比较运算。 ↩︎ ↩︎ ↩︎
builtin.error:Error() string方法,nil 表示没有错误。 ↩︎reflect.Value.IsNil:支持的 Kind 及对不支持种类、无效 Value 的 panic 边界。 ↩︎Go 数据竞争检测器,命令、环境要求及执行路径限制。 ↩︎